#include <test/jtx.h>
#include <test/jtx/credentials.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/trust.h>
#include <test/jtx/xchain_bridge.h>

#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>

namespace ripple {
namespace test {

class MPToken_test : public beast::unit_test::suite
{
    void
    testCreateValidation(FeatureBitset features)
    {
        testcase("Create Validate");
        using namespace test::jtx;
        Account const alice("alice");

        // test preflight of MPTokenIssuanceCreate
        {
            // If the MPT amendment is not enabled, you should not be able to
            // create MPTokenIssuances
            Env env{*this, features - featureMPTokensV1};
            MPTTester mptAlice(env, alice);

            mptAlice.create({.ownerCount = 0, .err = temDISABLED});
        }

        // test preflight of MPTokenIssuanceCreate
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice);

            mptAlice.create({.flags = 0x00000001, .err = temINVALID_FLAG});

            // tries to set a txfee while not enabling in the flag
            mptAlice.create(
                {.maxAmt = 100,
                 .assetScale = 0,
                 .transferFee = 1,
                 .metadata = "test",
                 .err = temMALFORMED});

            if (!features[featureSingleAssetVault])
            {
                // tries to set DomainID when SAV is disabled
                mptAlice.create(
                    {.maxAmt = 100,
                     .assetScale = 0,
                     .metadata = "test",
                     .flags = tfMPTRequireAuth,
                     .domainID = uint256(42),
                     .err = temDISABLED});
            }
            else if (!features[featurePermissionedDomains])
            {
                // tries to set DomainID when PD is disabled
                mptAlice.create(
                    {.maxAmt = 100,
                     .assetScale = 0,
                     .metadata = "test",
                     .flags = tfMPTRequireAuth,
                     .domainID = uint256(42),
                     .err = temDISABLED});
            }
            else
            {
                // tries to set DomainID when RequireAuth is not set
                mptAlice.create(
                    {.maxAmt = 100,
                     .assetScale = 0,
                     .metadata = "test",
                     .domainID = uint256(42),
                     .err = temMALFORMED});

                // tries to set zero DomainID
                mptAlice.create(
                    {.maxAmt = 100,
                     .assetScale = 0,
                     .metadata = "test",
                     .flags = tfMPTRequireAuth,
                     .domainID = beast::zero,
                     .err = temMALFORMED});
            }

            // tries to set a txfee greater than max
            mptAlice.create(
                {.maxAmt = 100,
                 .assetScale = 0,
                 .transferFee = maxTransferFee + 1,
                 .metadata = "test",
                 .flags = tfMPTCanTransfer,
                 .err = temBAD_TRANSFER_FEE});

            // tries to set a txfee while not enabling transfer
            mptAlice.create(
                {.maxAmt = 100,
                 .assetScale = 0,
                 .transferFee = maxTransferFee,
                 .metadata = "test",
                 .err = temMALFORMED});

            // empty metadata returns error
            mptAlice.create(
                {.maxAmt = 100,
                 .assetScale = 0,
                 .transferFee = 0,
                 .metadata = "",
                 .err = temMALFORMED});

            // MaximumAmout of 0 returns error
            mptAlice.create(
                {.maxAmt = 0,
                 .assetScale = 1,
                 .transferFee = 1,
                 .metadata = "test",
                 .err = temMALFORMED});

            // MaximumAmount larger than 63 bit returns error
            mptAlice.create(
                {.maxAmt = 0xFFFF'FFFF'FFFF'FFF0,  // 18'446'744'073'709'551'600
                 .assetScale = 0,
                 .transferFee = 0,
                 .metadata = "test",
                 .err = temMALFORMED});
            mptAlice.create(
                {.maxAmt = maxMPTokenAmount + 1,  // 9'223'372'036'854'775'808
                 .assetScale = 0,
                 .transferFee = 0,
                 .metadata = "test",
                 .err = temMALFORMED});
        }
    }

    void
    testCreateEnabled(FeatureBitset features)
    {
        testcase("Create Enabled");

        using namespace test::jtx;
        Account const alice("alice");

        {
            // If the MPT amendment IS enabled, you should be able to create
            // MPTokenIssuances
            Env env{*this, features};
            MPTTester mptAlice(env, alice);
            mptAlice.create(
                {.maxAmt = maxMPTokenAmount,  // 9'223'372'036'854'775'807
                 .assetScale = 1,
                 .transferFee = 10,
                 .metadata = "123",
                 .ownerCount = 1,
                 .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow |
                     tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback});

            // Get the hash for the most recent transaction.
            std::string const txHash{
                env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};

            Json::Value const result = env.rpc("tx", txHash)[jss::result];
            BEAST_EXPECT(
                result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
        }

        if (features[featureSingleAssetVault])
        {
            // Add permissioned domain
            Account const credIssuer1{"credIssuer1"};
            std::string const credType = "credential";

            pdomain::Credentials const credentials1{
                {.issuer = credIssuer1, .credType = credType}};

            {
                Env env{*this, features};
                env.fund(XRP(1000), credIssuer1);

                env(pdomain::setTx(credIssuer1, credentials1));
                auto const domainId1 = [&]() {
                    auto tx = env.tx()->getJson(JsonOptions::none);
                    return pdomain::getNewDomain(env.meta());
                }();

                MPTTester mptAlice(env, alice);
                mptAlice.create({
                    .maxAmt = maxMPTokenAmount,  // 9'223'372'036'854'775'807
                    .assetScale = 1,
                    .transferFee = 10,
                    .metadata = "123",
                    .ownerCount = 1,
                    .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow |
                        tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback,
                    .domainID = domainId1,
                });

                // Get the hash for the most recent transaction.
                std::string const txHash{
                    env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};

                Json::Value const result = env.rpc("tx", txHash)[jss::result];
                BEAST_EXPECT(
                    result[sfMaximumAmount.getJsonName()] ==
                    "9223372036854775807");
            }
        }
    }

    void
    testDestroyValidation(FeatureBitset features)
    {
        testcase("Destroy Validate");

        using namespace test::jtx;
        Account const alice("alice");
        Account const bob("bob");
        // MPTokenIssuanceDestroy (preflight)
        {
            Env env{*this, features - featureMPTokensV1};
            MPTTester mptAlice(env, alice);
            auto const id = makeMptID(env.seq(alice), alice);
            mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED});

            env.enableFeature(featureMPTokensV1);

            mptAlice.destroy(
                {.id = id, .flags = 0x00000001, .err = temINVALID_FLAG});
        }

        // MPTokenIssuanceDestroy (preclaim)
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.destroy(
                {.id = makeMptID(env.seq(alice), alice),
                 .ownerCount = 0,
                 .err = tecOBJECT_NOT_FOUND});

            mptAlice.create({.ownerCount = 1});

            // a non-issuer tries to destroy a mptissuance they didn't issue
            mptAlice.destroy({.issuer = bob, .err = tecNO_PERMISSION});

            // Make sure that issuer can't delete issuance when it still has
            // outstanding balance
            {
                // bob now holds a mptoken object
                mptAlice.authorize({.account = bob, .holderCount = 1});

                // alice pays bob 100 tokens
                mptAlice.pay(alice, bob, 100);

                mptAlice.destroy({.err = tecHAS_OBLIGATIONS});
            }
        }
    }

    void
    testDestroyEnabled(FeatureBitset features)
    {
        testcase("Destroy Enabled");

        using namespace test::jtx;
        Account const alice("alice");

        // If the MPT amendment IS enabled, you should be able to destroy
        // MPTokenIssuances
        Env env{*this, features};
        MPTTester mptAlice(env, alice);

        mptAlice.create({.ownerCount = 1});

        mptAlice.destroy({.ownerCount = 0});
    }

    void
    testAuthorizeValidation(FeatureBitset features)
    {
        testcase("Validate authorize transaction");

        using namespace test::jtx;
        Account const alice("alice");
        Account const bob("bob");
        Account const cindy("cindy");
        // Validate amendment enable in MPTokenAuthorize (preflight)
        {
            Env env{*this, features - featureMPTokensV1};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.authorize(
                {.account = bob,
                 .id = makeMptID(env.seq(alice), alice),
                 .err = temDISABLED});
        }

        // Validate fields in MPTokenAuthorize (preflight)
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1});

            // The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which
            // has a value of 1
            mptAlice.authorize(
                {.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG});

            mptAlice.authorize(
                {.account = bob, .holder = bob, .err = temMALFORMED});

            mptAlice.authorize({.holder = alice, .err = temMALFORMED});
        }

        // Try authorizing when MPTokenIssuance doesn't exist in
        // MPTokenAuthorize (preclaim)
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            auto const id = makeMptID(env.seq(alice), alice);

            mptAlice.authorize(
                {.holder = bob, .id = id, .err = tecOBJECT_NOT_FOUND});

            mptAlice.authorize(
                {.account = bob, .id = id, .err = tecOBJECT_NOT_FOUND});
        }

        // Test bad scenarios without allowlisting in MPTokenAuthorize
        // (preclaim)
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1});

            // bob submits a tx with a holder field
            mptAlice.authorize(
                {.account = bob, .holder = alice, .err = tecNO_PERMISSION});

            // alice tries to hold onto her own token
            mptAlice.authorize({.account = alice, .err = tecNO_PERMISSION});

            // the mpt does not enable allowlisting
            mptAlice.authorize({.holder = bob, .err = tecNO_AUTH});

            // bob now holds a mptoken object
            mptAlice.authorize({.account = bob, .holderCount = 1});

            // bob cannot create the mptoken the second time
            mptAlice.authorize({.account = bob, .err = tecDUPLICATE});

            // Check that bob cannot delete MPToken when his balance is
            // non-zero
            {
                // alice pays bob 100 tokens
                mptAlice.pay(alice, bob, 100);

                // bob tries to delete his MPToken, but fails since he still
                // holds tokens
                mptAlice.authorize(
                    {.account = bob,
                     .flags = tfMPTUnauthorize,
                     .err = tecHAS_OBLIGATIONS});

                // bob pays back alice 100 tokens
                mptAlice.pay(bob, alice, 100);
            }

            // bob deletes/unauthorizes his MPToken
            mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});

            // bob receives error when he tries to delete his MPToken that has
            // already been deleted
            mptAlice.authorize(
                {.account = bob,
                 .holderCount = 0,
                 .flags = tfMPTUnauthorize,
                 .err = tecOBJECT_NOT_FOUND});
        }

        // Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim)
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});

            // alice submits a tx without specifying a holder's account
            mptAlice.authorize({.err = tecNO_PERMISSION});

            // alice submits a tx to authorize a holder that hasn't created
            // a mptoken yet
            mptAlice.authorize({.holder = bob, .err = tecOBJECT_NOT_FOUND});

            // alice specifys a holder acct that doesn't exist
            mptAlice.authorize({.holder = cindy, .err = tecNO_DST});

            // bob now holds a mptoken object
            mptAlice.authorize({.account = bob, .holderCount = 1});

            // alice tries to unauthorize bob.
            // although tx is successful,
            // but nothing happens because bob hasn't been authorized yet
            mptAlice.authorize({.holder = bob, .flags = tfMPTUnauthorize});

            // alice authorizes bob
            // make sure bob's mptoken has set lsfMPTAuthorized
            mptAlice.authorize({.holder = bob});

            // alice tries authorizes bob again.
            // tx is successful, but bob is already authorized,
            // so no changes
            mptAlice.authorize({.holder = bob});

            // bob deletes his mptoken
            mptAlice.authorize(
                {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
        }

        // Test mptoken reserve requirement - first two mpts free (doApply)
        {
            Env env{*this, features};
            auto const acctReserve = env.current()->fees().reserve;
            auto const incReserve = env.current()->fees().increment;

            // 1 drop
            BEAST_EXPECT(incReserve > XRPAmount(1));
            MPTTester mptAlice1(
                env,
                alice,
                {.holders = {bob},
                 .xrpHolders = acctReserve + (incReserve - 1)});
            mptAlice1.create();

            MPTTester mptAlice2(env, alice, {.fund = false});
            mptAlice2.create();

            MPTTester mptAlice3(env, alice, {.fund = false});
            mptAlice3.create({.ownerCount = 3});

            // first mpt for free
            mptAlice1.authorize({.account = bob, .holderCount = 1});

            // second mpt free
            mptAlice2.authorize({.account = bob, .holderCount = 2});

            mptAlice3.authorize(
                {.account = bob, .err = tecINSUFFICIENT_RESERVE});

            env(pay(
                env.master, bob, drops(incReserve + incReserve + incReserve)));
            env.close();

            mptAlice3.authorize({.account = bob, .holderCount = 3});
        }
    }

    void
    testAuthorizeEnabled(FeatureBitset features)
    {
        testcase("Authorize Enabled");

        using namespace test::jtx;
        Account const alice("alice");
        Account const bob("bob");
        // Basic authorization without allowlisting
        {
            Env env{*this, features};

            // alice create mptissuance without allowisting
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1});

            // bob creates a mptoken
            mptAlice.authorize({.account = bob, .holderCount = 1});

            mptAlice.authorize(
                {.account = bob, .holderCount = 1, .err = tecDUPLICATE});

            // bob deletes his mptoken
            mptAlice.authorize(
                {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
        }

        // With allowlisting
        {
            Env env{*this, features};

            // alice creates a mptokenissuance that requires authorization
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});

            // bob creates a mptoken
            mptAlice.authorize({.account = bob, .holderCount = 1});

            // alice authorizes bob
            mptAlice.authorize({.account = alice, .holder = bob});

            // Unauthorize bob's mptoken
            mptAlice.authorize(
                {.account = alice,
                 .holder = bob,
                 .holderCount = 1,
                 .flags = tfMPTUnauthorize});

            mptAlice.authorize(
                {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
        }

        // Holder can have dangling MPToken even if issuance has been destroyed.
        // Make sure they can still delete/unauthorize the MPToken
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1});

            // bob creates a mptoken
            mptAlice.authorize({.account = bob, .holderCount = 1});

            // alice deletes her issuance
            mptAlice.destroy({.ownerCount = 0});

            // bob can delete his mptoken even though issuance is no longer
            // existent
            mptAlice.authorize(
                {.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
        }
    }

    void
    testSetValidation(FeatureBitset features)
    {
        testcase("Validate set transaction");

        using namespace test::jtx;
        Account const alice("alice");  // issuer
        Account const bob("bob");      // holder
        Account const cindy("cindy");
        // Validate fields in MPTokenIssuanceSet (preflight)
        {
            Env env{*this, features - featureMPTokensV1};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.set(
                {.account = bob,
                 .id = makeMptID(env.seq(alice), alice),
                 .err = temDISABLED});

            env.enableFeature(featureMPTokensV1);

            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            mptAlice.authorize({.account = bob, .holderCount = 1});

            // test invalid flag - only valid flags are tfMPTLock (1) and Unlock
            // (2)
            mptAlice.set(
                {.account = alice,
                 .flags = 0x00000008,
                 .err = temINVALID_FLAG});

            if (!features[featureSingleAssetVault] &&
                !features[featureDynamicMPT])
            {
                // test invalid flags - nothing is being changed
                mptAlice.set(
                    {.account = alice,
                     .flags = 0x00000000,
                     .err = tecNO_PERMISSION});

                mptAlice.set(
                    {.account = alice,
                     .holder = bob,
                     .flags = 0x00000000,
                     .err = tecNO_PERMISSION});

                // cannot set DomainID since SAV is not enabled
                mptAlice.set(
                    {.account = alice,
                     .domainID = uint256(42),
                     .err = temDISABLED});
            }
            else
            {
                // test invalid flags - nothing is being changed
                mptAlice.set(
                    {.account = alice,
                     .flags = 0x00000000,
                     .err = temMALFORMED});

                mptAlice.set(
                    {.account = alice,
                     .holder = bob,
                     .flags = 0x00000000,
                     .err = temMALFORMED});

                if (!features[featurePermissionedDomains] ||
                    !features[featureSingleAssetVault])
                {
                    // cannot set DomainID since PD is not enabled
                    mptAlice.set(
                        {.account = alice,
                         .domainID = uint256(42),
                         .err = temDISABLED});
                }
                else if (features[featureSingleAssetVault])
                {
                    // cannot set DomainID since Holder is set
                    mptAlice.set(
                        {.account = alice,
                         .holder = bob,
                         .domainID = uint256(42),
                         .err = temMALFORMED});
                }
            }

            // set both lock and unlock flags at the same time will fail
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTLock | tfMPTUnlock,
                 .err = temINVALID_FLAG});

            // if the holder is the same as the acct that submitted the tx,
            // tx fails
            mptAlice.set(
                {.account = alice,
                 .holder = alice,
                 .flags = tfMPTLock,
                 .err = temMALFORMED});
        }

        // Validate fields in MPTokenIssuanceSet (preclaim)
        // test when a mptokenissuance has disabled locking
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1});

            // alice tries to lock a mptissuance that has disabled locking
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTLock,
                 .err = tecNO_PERMISSION});

            // alice tries to unlock mptissuance that has disabled locking
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTUnlock,
                 .err = tecNO_PERMISSION});

            // issuer tries to lock a bob's mptoken that has disabled
            // locking
            mptAlice.set(
                {.account = alice,
                 .holder = bob,
                 .flags = tfMPTLock,
                 .err = tecNO_PERMISSION});

            // issuer tries to unlock a bob's mptoken that has disabled
            // locking
            mptAlice.set(
                {.account = alice,
                 .holder = bob,
                 .flags = tfMPTUnlock,
                 .err = tecNO_PERMISSION});
        }

        // Validate fields in MPTokenIssuanceSet (preclaim)
        // test when mptokenissuance has enabled locking
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // alice trying to set when the mptissuance doesn't exist yet
            mptAlice.set(
                {.id = makeMptID(env.seq(alice), alice),
                 .flags = tfMPTLock,
                 .err = tecOBJECT_NOT_FOUND});

            // create a mptokenissuance with locking
            mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock});

            // a non-issuer acct tries to set the mptissuance
            mptAlice.set(
                {.account = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});

            // trying to set a holder who doesn't have a mptoken
            mptAlice.set(
                {.holder = bob,
                 .flags = tfMPTLock,
                 .err = tecOBJECT_NOT_FOUND});

            // trying to set a holder who doesn't exist
            mptAlice.set(
                {.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST});
        }

        if (features[featureSingleAssetVault] &&
            features[featurePermissionedDomains])
        {
            // Add permissioned domain
            Account const credIssuer1{"credIssuer1"};
            std::string const credType = "credential";

            pdomain::Credentials const credentials1{
                {.issuer = credIssuer1, .credType = credType}};

            {
                Env env{*this, features};

                MPTTester mptAlice(env, alice);
                mptAlice.create({});

                // Trying to set DomainID on a public MPTokenIssuance
                mptAlice.set(
                    {.domainID = uint256(42), .err = tecNO_PERMISSION});

                mptAlice.set(
                    {.domainID = beast::zero, .err = tecNO_PERMISSION});
            }

            {
                Env env{*this, features};

                MPTTester mptAlice(env, alice);
                mptAlice.create({.flags = tfMPTRequireAuth});

                // Trying to set non-existing DomainID
                mptAlice.set(
                    {.domainID = uint256(42), .err = tecOBJECT_NOT_FOUND});

                // Trying to lock but locking is disabled
                mptAlice.set(
                    {.flags = tfMPTUnlock,
                     .domainID = uint256(42),
                     .err = tecNO_PERMISSION});

                mptAlice.set(
                    {.flags = tfMPTUnlock,
                     .domainID = beast::zero,
                     .err = tecNO_PERMISSION});
            }
        }
    }

    void
    testSetEnabled(FeatureBitset features)
    {
        testcase("Enabled set transaction");

        using namespace test::jtx;
        Account const alice("alice");  // issuer
        Account const bob("bob");      // holder

        {
            // Test locking and unlocking
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // create a mptokenissuance with locking
            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});

            mptAlice.authorize({.account = bob, .holderCount = 1});

            // locks bob's mptoken
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});

            // trying to lock bob's mptoken again will still succeed
            // but no changes to the objects
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});

            // alice locks the mptissuance
            mptAlice.set({.account = alice, .flags = tfMPTLock});

            // alice tries to lock up both mptissuance and mptoken again
            // it will not change the flags and both will remain locked.
            mptAlice.set({.account = alice, .flags = tfMPTLock});
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});

            // alice unlocks bob's mptoken
            mptAlice.set(
                {.account = alice, .holder = bob, .flags = tfMPTUnlock});

            // locks up bob's mptoken again
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
            if (!features[featureSingleAssetVault])
            {
                // Delete bobs' mptoken even though it is locked
                mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});

                mptAlice.set(
                    {.account = alice,
                     .holder = bob,
                     .flags = tfMPTUnlock,
                     .err = tecOBJECT_NOT_FOUND});

                return;
            }

            // Cannot delete locked MPToken
            mptAlice.authorize(
                {.account = bob,
                 .flags = tfMPTUnauthorize,
                 .err = tecNO_PERMISSION});

            // alice unlocks mptissuance
            mptAlice.set({.account = alice, .flags = tfMPTUnlock});

            // alice unlocks bob's mptoken
            mptAlice.set(
                {.account = alice, .holder = bob, .flags = tfMPTUnlock});

            // alice unlocks mptissuance and bob's mptoken again despite that
            // they are already unlocked. Make sure this will not change the
            // flags
            mptAlice.set(
                {.account = alice, .holder = bob, .flags = tfMPTUnlock});
            mptAlice.set({.account = alice, .flags = tfMPTUnlock});
        }

        if (features[featureSingleAssetVault])
        {
            // Add permissioned domain
            std::string const credType = "credential";

            // Test setting and resetting domain ID
            Env env{*this, features};

            auto const domainId1 = [&]() {
                Account const credIssuer1{"credIssuer1"};
                env.fund(XRP(1000), credIssuer1);

                pdomain::Credentials const credentials1{
                    {.issuer = credIssuer1, .credType = credType}};

                env(pdomain::setTx(credIssuer1, credentials1));
                return [&]() {
                    auto tx = env.tx()->getJson(JsonOptions::none);
                    return pdomain::getNewDomain(env.meta());
                }();
            }();

            auto const domainId2 = [&]() {
                Account const credIssuer2{"credIssuer2"};
                env.fund(XRP(1000), credIssuer2);

                pdomain::Credentials const credentials2{
                    {.issuer = credIssuer2, .credType = credType}};

                env(pdomain::setTx(credIssuer2, credentials2));
                return [&]() {
                    auto tx = env.tx()->getJson(JsonOptions::none);
                    return pdomain::getNewDomain(env.meta());
                }();
            }();

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // create a mptokenissuance with auth.
            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth});
            BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));

            // reset "domain not set" to "domain not set", i.e. no change
            mptAlice.set({.domainID = beast::zero});
            BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));

            // reset "domain not set" to domain1
            mptAlice.set({.domainID = domainId1});
            BEAST_EXPECT(mptAlice.checkDomainID(domainId1));

            // reset domain1 to domain2
            mptAlice.set({.domainID = domainId2});
            BEAST_EXPECT(mptAlice.checkDomainID(domainId2));

            // reset domain to "domain not set"
            mptAlice.set({.domainID = beast::zero});
            BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
        }
    }

    void
    testPayment(FeatureBitset features)
    {
        testcase("Payment");

        using namespace test::jtx;
        Account const alice("alice");  // issuer
        Account const bob("bob");      // holder
        Account const carol("carol");  // holder

        // preflight validation

        // MPT is disabled
        {
            Env env{*this, features - featureMPTokensV1};
            Account const alice("alice");
            Account const bob("bob");

            env.fund(XRP(1'000), alice);
            env.fund(XRP(1'000), bob);
            STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};

            env(pay(alice, bob, mpt), ter(temDISABLED));
        }

        // MPT is disabled, unsigned request
        {
            Env env{*this, features - featureMPTokensV1};
            Account const alice("alice");  // issuer
            Account const carol("carol");
            auto const USD = alice["USD"];

            env.fund(XRP(1'000), alice);
            env.fund(XRP(1'000), carol);
            STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};

            Json::Value jv;
            jv[jss::secret] = alice.name();
            jv[jss::tx_json] = pay(alice, carol, mpt);
            jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
            auto const jrr = env.rpc("json", "submit", to_string(jv));
            BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED");
        }

        // Invalid flag
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});
            auto const MPT = mptAlice["MPT"];

            mptAlice.authorize({.account = bob});

            for (auto flags : {tfNoRippleDirect, tfLimitQuality})
                env(pay(alice, bob, MPT(10)),
                    txflags(flags),
                    ter(temINVALID_FLAG));
        }

        // Invalid combination of send, sendMax, deliverMin, paths
        {
            Env env{*this, features};
            Account const alice("alice");
            Account const carol("carol");

            MPTTester mptAlice(env, alice, {.holders = {carol}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            mptAlice.authorize({.account = carol});

            // sendMax and DeliverMin are valid XRP amount,
            // but is invalid combination with MPT amount
            auto const MPT = mptAlice["MPT"];
            env(pay(alice, carol, MPT(100)),
                sendmax(XRP(100)),
                ter(temMALFORMED));
            env(pay(alice, carol, MPT(100)),
                delivermin(XRP(100)),
                ter(temBAD_AMOUNT));
            // sendMax MPT is invalid with IOU or XRP
            auto const USD = alice["USD"];
            env(pay(alice, carol, USD(100)),
                sendmax(MPT(100)),
                ter(temMALFORMED));
            env(pay(alice, carol, XRP(100)),
                sendmax(MPT(100)),
                ter(temMALFORMED));
            env(pay(alice, carol, USD(100)),
                delivermin(MPT(100)),
                ter(temBAD_AMOUNT));
            env(pay(alice, carol, XRP(100)),
                delivermin(MPT(100)),
                ter(temBAD_AMOUNT));
            // sendmax and amount are different MPT issue
            test::jtx::MPT const MPT1(
                "MPT", makeMptID(env.seq(alice) + 10, alice));
            env(pay(alice, carol, MPT1(100)),
                sendmax(MPT(100)),
                ter(temMALFORMED));
            // paths is invalid
            env(pay(alice, carol, MPT(100)), path(~USD), ter(temMALFORMED));
        }

        // build_path is invalid if MPT
        {
            Env env{*this, features};
            Account const alice("alice");
            Account const carol("carol");

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});
            auto const MPT = mptAlice["MPT"];

            mptAlice.authorize({.account = carol});

            Json::Value payment;
            payment[jss::secret] = alice.name();
            payment[jss::tx_json] = pay(alice, carol, MPT(100));

            payment[jss::build_path] = true;
            auto jrr = env.rpc("json", "submit", to_string(payment));
            BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
            BEAST_EXPECT(
                jrr[jss::result][jss::error_message] ==
                "Field 'build_path' not allowed in this context.");
        }

        // Can't pay negative amount
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});
            auto const MPT = mptAlice["MPT"];

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            mptAlice.pay(alice, bob, -1, temBAD_AMOUNT);

            mptAlice.pay(bob, carol, -1, temBAD_AMOUNT);

            mptAlice.pay(bob, alice, -1, temBAD_AMOUNT);

            env(pay(alice, bob, MPT(10)), sendmax(MPT(-1)), ter(temBAD_AMOUNT));
        }

        // Pay to self
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            mptAlice.authorize({.account = bob});

            mptAlice.pay(bob, bob, 10, temREDUNDANT);
        }

        // preclaim validation

        // Destination doesn't exist
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            mptAlice.authorize({.account = bob});

            Account const bad{"bad"};
            env.memoize(bad);

            mptAlice.pay(bob, bad, 10, tecNO_DST);
        }

        // apply validation

        // If RequireAuth is enabled, Payment fails if the receiver is not
        // authorized
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTRequireAuth | tfMPTCanTransfer});

            mptAlice.authorize({.account = bob});

            mptAlice.pay(alice, bob, 100, tecNO_AUTH);
        }

        // If RequireAuth is enabled, Payment fails if the sender is not
        // authorized
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTRequireAuth | tfMPTCanTransfer});

            // bob creates an empty MPToken
            mptAlice.authorize({.account = bob});

            // alice authorizes bob to hold funds
            mptAlice.authorize({.account = alice, .holder = bob});

            // alice sends 100 MPT to bob
            mptAlice.pay(alice, bob, 100);

            // alice UNAUTHORIZES bob
            mptAlice.authorize(
                {.account = alice, .holder = bob, .flags = tfMPTUnauthorize});

            // bob fails to send back to alice because he is no longer
            // authorize to move his funds!
            mptAlice.pay(bob, alice, 100, tecNO_AUTH);
        }

        if (features[featureSingleAssetVault] &&
            features[featurePermissionedDomains])
        {
            // If RequireAuth is enabled and domain is a match, payment succeeds
            {
                Env env{*this, features};
                std::string const credType = "credential";
                Account const credIssuer1{"credIssuer1"};
                env.fund(XRP(1000), credIssuer1, bob);

                auto const domainId1 = [&]() {
                    pdomain::Credentials const credentials1{
                        {.issuer = credIssuer1, .credType = credType}};

                    env(pdomain::setTx(credIssuer1, credentials1));
                    return [&]() {
                        auto tx = env.tx()->getJson(JsonOptions::none);
                        return pdomain::getNewDomain(env.meta());
                    }();
                }();
                // bob is authorized via domain
                env(credentials::create(bob, credIssuer1, credType));
                env(credentials::accept(bob, credIssuer1, credType));
                env.close();

                MPTTester mptAlice(env, alice);
                env.close();

                mptAlice.create({
                    .ownerCount = 1,
                    .holderCount = 0,
                    .flags = tfMPTRequireAuth | tfMPTCanTransfer,
                    .domainID = domainId1,
                });

                mptAlice.authorize({.account = bob});
                env.close();

                // bob is authorized via domain
                mptAlice.pay(alice, bob, 100);
                mptAlice.set({.domainID = beast::zero});

                // bob is no longer authorized
                mptAlice.pay(alice, bob, 100, tecNO_AUTH);
            }

            {
                Env env{*this, features};
                std::string const credType = "credential";
                Account const credIssuer1{"credIssuer1"};
                env.fund(XRP(1000), credIssuer1, bob);

                auto const domainId1 = [&]() {
                    pdomain::Credentials const credentials1{
                        {.issuer = credIssuer1, .credType = credType}};

                    env(pdomain::setTx(credIssuer1, credentials1));
                    return [&]() {
                        auto tx = env.tx()->getJson(JsonOptions::none);
                        return pdomain::getNewDomain(env.meta());
                    }();
                }();
                // bob is authorized via domain
                env(credentials::create(bob, credIssuer1, credType));
                env(credentials::accept(bob, credIssuer1, credType));
                env.close();

                MPTTester mptAlice(env, alice);
                env.close();

                mptAlice.create({
                    .ownerCount = 1,
                    .holderCount = 0,
                    .flags = tfMPTRequireAuth | tfMPTCanTransfer,
                    .domainID = domainId1,
                });

                // bob creates an empty MPToken
                mptAlice.authorize({.account = bob});

                // alice authorizes bob to hold funds
                mptAlice.authorize({.account = alice, .holder = bob});

                // alice sends 100 MPT to bob
                mptAlice.pay(alice, bob, 100);

                // alice UNAUTHORIZES bob
                mptAlice.authorize(
                    {.account = alice,
                     .holder = bob,
                     .flags = tfMPTUnauthorize});

                // bob is still authorized, via domain
                mptAlice.pay(bob, alice, 10);

                mptAlice.set({.domainID = beast::zero});

                // bob fails to send back to alice because he is no longer
                // authorize to move his funds!
                mptAlice.pay(bob, alice, 10, tecNO_AUTH);
            }

            {
                Env env{*this, features};
                std::string const credType = "credential";
                // credIssuer1 is the owner of domainId1 and a credential issuer
                Account const credIssuer1{"credIssuer1"};
                // credIssuer2 is the owner of domainId2 and a credential issuer
                // Note, domainId2 also lists credentials issued by credIssuer1
                Account const credIssuer2{"credIssuer2"};
                env.fund(XRP(1000), credIssuer1, credIssuer2, bob, carol);

                auto const domainId1 = [&]() {
                    pdomain::Credentials const credentials{
                        {.issuer = credIssuer1, .credType = credType}};

                    env(pdomain::setTx(credIssuer1, credentials));
                    return [&]() {
                        auto tx = env.tx()->getJson(JsonOptions::none);
                        return pdomain::getNewDomain(env.meta());
                    }();
                }();

                auto const domainId2 = [&]() {
                    pdomain::Credentials const credentials{
                        {.issuer = credIssuer1, .credType = credType},
                        {.issuer = credIssuer2, .credType = credType}};

                    env(pdomain::setTx(credIssuer2, credentials));
                    return [&]() {
                        auto tx = env.tx()->getJson(JsonOptions::none);
                        return pdomain::getNewDomain(env.meta());
                    }();
                }();

                // bob is authorized via credIssuer1 which is recognized by both
                // domainId1 and domainId2
                env(credentials::create(bob, credIssuer1, credType));
                env(credentials::accept(bob, credIssuer1, credType));
                env.close();

                // carol is authorized via credIssuer2, only recognized by
                // domainId2
                env(credentials::create(carol, credIssuer2, credType));
                env(credentials::accept(carol, credIssuer2, credType));
                env.close();

                MPTTester mptAlice(env, alice);
                env.close();

                mptAlice.create({
                    .ownerCount = 1,
                    .holderCount = 0,
                    .flags = tfMPTRequireAuth | tfMPTCanTransfer,
                    .domainID = domainId1,
                });

                // bob and carol create an empty MPToken
                mptAlice.authorize({.account = bob});
                mptAlice.authorize({.account = carol});
                env.close();

                // alice sends 50 MPT to bob but cannot send to carol
                mptAlice.pay(alice, bob, 50);
                mptAlice.pay(alice, carol, 50, tecNO_AUTH);
                env.close();

                // bob cannot send to carol because they are not on the same
                // domain (since credIssuer2 is not recognized by domainId1)
                mptAlice.pay(bob, carol, 10, tecNO_AUTH);
                env.close();

                // alice updates domainID to domainId2 which recognizes both
                // credIssuer1 and credIssuer2
                mptAlice.set({.domainID = domainId2});
                // alice can now send to carol
                mptAlice.pay(alice, carol, 10);
                env.close();

                // bob can now send to carol because both are in the same
                // domain
                mptAlice.pay(bob, carol, 10);
                env.close();

                // bob loses his authorization and can no longer send MPT
                env(credentials::deleteCred(
                    credIssuer1, bob, credIssuer1, credType));
                env.close();

                mptAlice.pay(bob, carol, 10, tecNO_AUTH);
                mptAlice.pay(bob, alice, 10, tecNO_AUTH);
            }
        }

        // Non-issuer cannot send to each other if MPTCanTransfer isn't set
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};
            Account const cindy{"cindy"};

            MPTTester mptAlice(env, alice, {.holders = {bob, cindy}});

            // alice creates issuance without MPTCanTransfer
            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            // bob creates a MPToken
            mptAlice.authorize({.account = bob});

            // cindy creates a MPToken
            mptAlice.authorize({.account = cindy});

            // alice pays bob 100 tokens
            mptAlice.pay(alice, bob, 100);

            // bob tries to send cindy 10 tokens, but fails because canTransfer
            // is off
            mptAlice.pay(bob, cindy, 10, tecNO_AUTH);

            // bob can send back to alice(issuer) just fine
            mptAlice.pay(bob, alice, 10);
        }

        // Holder is not authorized
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});

            // issuer to holder
            mptAlice.pay(alice, bob, 100, tecNO_AUTH);

            // holder to issuer
            mptAlice.pay(bob, alice, 100, tecNO_AUTH);

            // holder to holder
            mptAlice.pay(bob, carol, 50, tecNO_AUTH);
        }

        // Payer doesn't have enough funds
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            mptAlice.pay(alice, bob, 100);

            // Pay to another holder
            mptAlice.pay(bob, carol, 101, tecPATH_PARTIAL);

            // Pay to the issuer
            mptAlice.pay(bob, alice, 101, tecPATH_PARTIAL);
        }

        // MPT is locked
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer});

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            mptAlice.pay(alice, bob, 100);
            mptAlice.pay(alice, carol, 100);

            // Global lock
            mptAlice.set({.account = alice, .flags = tfMPTLock});
            // Can't send between holders
            mptAlice.pay(bob, carol, 1, tecLOCKED);
            mptAlice.pay(carol, bob, 2, tecLOCKED);
            // Issuer can send
            mptAlice.pay(alice, bob, 3);
            // Holder can send back to issuer
            mptAlice.pay(bob, alice, 4);

            // Global unlock
            mptAlice.set({.account = alice, .flags = tfMPTUnlock});
            // Individual lock
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
            // Can't send between holders
            mptAlice.pay(bob, carol, 5, tecLOCKED);
            mptAlice.pay(carol, bob, 6, tecLOCKED);
            // Issuer can send
            mptAlice.pay(alice, bob, 7);
            // Holder can send back to issuer
            mptAlice.pay(bob, alice, 8);
        }

        // Transfer fee
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            // Transfer fee is 10%
            mptAlice.create(
                {.transferFee = 10'000,
                 .ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanTransfer});

            // Holders create MPToken
            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            // Payment between the issuer and the holder, no transfer fee.
            mptAlice.pay(alice, bob, 2'000);

            // Payment between the holder and the issuer, no transfer fee.
            mptAlice.pay(bob, alice, 1'000);
            BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 1'000));

            // Payment between the holders. The sender doesn't have
            // enough funds to cover the transfer fee.
            mptAlice.pay(bob, carol, 1'000, tecPATH_PARTIAL);

            // Payment between the holders. The sender has enough funds
            // but SendMax is not included.
            mptAlice.pay(bob, carol, 100, tecPATH_PARTIAL);

            auto const MPT = mptAlice["MPT"];
            // SendMax doesn't cover the fee
            env(pay(bob, carol, MPT(100)),
                sendmax(MPT(109)),
                ter(tecPATH_PARTIAL));

            // Payment succeeds if sufficient SendMax is included.
            // 100 to carol, 10 to issuer
            env(pay(bob, carol, MPT(100)), sendmax(MPT(110)));
            // 100 to carol, 10 to issuer
            env(pay(bob, carol, MPT(100)), sendmax(MPT(115)));
            BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 780));
            BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 200));
            // Payment succeeds if partial payment even if
            // SendMax is less than deliver amount
            env(pay(bob, carol, MPT(100)),
                sendmax(MPT(90)),
                txflags(tfPartialPayment));
            // 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) =
            // 82)
            BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
            BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 282));
        }

        // Insufficient SendMax with no transfer fee
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});

            // Holders create MPToken
            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});
            mptAlice.pay(alice, bob, 1'000);

            auto const MPT = mptAlice["MPT"];
            // SendMax is less than the amount
            env(pay(bob, carol, MPT(100)),
                sendmax(MPT(99)),
                ter(tecPATH_PARTIAL));
            env(pay(bob, alice, MPT(100)),
                sendmax(MPT(99)),
                ter(tecPATH_PARTIAL));

            // Payment succeeds if sufficient SendMax is included.
            env(pay(bob, carol, MPT(100)), sendmax(MPT(100)));
            BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 100));
            // Payment succeeds if partial payment
            env(pay(bob, carol, MPT(100)),
                sendmax(MPT(99)),
                txflags(tfPartialPayment));
            BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 199));
        }

        // DeliverMin
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});

            // Holders create MPToken
            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});
            mptAlice.pay(alice, bob, 1'000);

            auto const MPT = mptAlice["MPT"];
            // Fails even with the partial payment because
            // deliver amount < deliverMin
            env(pay(bob, alice, MPT(100)),
                sendmax(MPT(99)),
                delivermin(MPT(100)),
                txflags(tfPartialPayment),
                ter(tecPATH_PARTIAL));
            // Payment succeeds if deliver amount >= deliverMin
            env(pay(bob, alice, MPT(100)),
                sendmax(MPT(99)),
                delivermin(MPT(99)),
                txflags(tfPartialPayment));
        }

        // Issuer fails trying to send more than the maximum amount allowed
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.maxAmt = 100,
                 .ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanTransfer});

            mptAlice.authorize({.account = bob});

            // issuer sends holder the max amount allowed
            mptAlice.pay(alice, bob, 100);

            // issuer tries to exceed max amount
            mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL);
        }

        // Issuer fails trying to send more than the default maximum
        // amount allowed
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            mptAlice.authorize({.account = bob});

            // issuer sends holder the default max amount allowed
            mptAlice.pay(alice, bob, maxMPTokenAmount);

            // issuer tries to exceed max amount
            mptAlice.pay(alice, bob, 1, tecPATH_PARTIAL);
        }

        // Pay more than max amount fails in the json parser before
        // transactor is called
        {
            Env env{*this, features};
            env.fund(XRP(1'000), alice, bob);
            STAmount mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
            Json::Value jv;
            jv[jss::secret] = alice.name();
            jv[jss::tx_json] = pay(alice, bob, mpt);
            jv[jss::tx_json][jss::Amount][jss::value] =
                to_string(maxMPTokenAmount + 1);
            auto const jrr = env.rpc("json", "submit", to_string(jv));
            BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
        }

        // Pay maximum amount with the transfer fee, SendMax, and
        // partial payment
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.maxAmt = 10'000,
                 .transferFee = 100,
                 .ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanTransfer});
            auto const MPT = mptAlice["MPT"];

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            // issuer sends holder the max amount allowed
            mptAlice.pay(alice, bob, 10'000);

            // payment between the holders
            env(pay(bob, carol, MPT(10'000)),
                sendmax(MPT(10'000)),
                txflags(tfPartialPayment));
            // Verify the metadata
            auto const meta = env.meta()->getJson(
                JsonOptions::none)[sfAffectedNodes.fieldName];
            // Issuer got 10 in the transfer fees
            BEAST_EXPECT(
                meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
                    [sfOutstandingAmount.fieldName] == "9990");
            // Destination account got 9'990
            BEAST_EXPECT(
                meta[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
                    [sfMPTAmount.fieldName] == "9990");
            // Source account spent 10'000
            BEAST_EXPECT(
                meta[2u][sfModifiedNode.fieldName][sfPreviousFields.fieldName]
                    [sfMPTAmount.fieldName] == "10000");
            BEAST_EXPECT(
                !meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
                     .isMember(sfMPTAmount.fieldName));

            // payment between the holders fails without
            // partial payment
            env(pay(bob, carol, MPT(10'000)),
                sendmax(MPT(10'000)),
                ter(tecPATH_PARTIAL));
        }

        // Pay maximum allowed amount
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.maxAmt = maxMPTokenAmount,
                 .ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanTransfer});
            auto const MPT = mptAlice["MPT"];

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            // issuer sends holder the max amount allowed
            mptAlice.pay(alice, bob, maxMPTokenAmount);
            BEAST_EXPECT(
                mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount));

            // payment between the holders
            mptAlice.pay(bob, carol, maxMPTokenAmount);
            BEAST_EXPECT(
                mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount));
            // holder pays back to the issuer
            mptAlice.pay(carol, alice, maxMPTokenAmount);
            BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(0));
        }

        // Issuer fails trying to send fund after issuance was destroyed
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            mptAlice.authorize({.account = bob});

            // alice destroys issuance
            mptAlice.destroy({.ownerCount = 0});

            // alice tries to send bob fund after issuance is destroyed, should
            // fail.
            mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
        }

        // Non-existent issuance
        {
            Env env{*this, features};

            env.fund(XRP(1'000), alice, bob);

            STAmount const mpt{MPTID{0}, 100};
            env(pay(alice, bob, mpt), ter(tecOBJECT_NOT_FOUND));
        }

        // Issuer fails trying to send to an account, which doesn't own MPT for
        // an issuance that was destroyed
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            // alice destroys issuance
            mptAlice.destroy({.ownerCount = 0});

            // alice tries to send bob who doesn't own the MPT after issuance is
            // destroyed, it should fail
            mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
        }

        // Issuers issues maximum amount of MPT to a holder, the holder should
        // be able to transfer the max amount to someone else
        {
            Env env{*this, features};
            Account const alice("alice");
            Account const carol("bob");
            Account const bob("carol");

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.maxAmt = 100, .ownerCount = 1, .flags = tfMPTCanTransfer});

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            mptAlice.pay(alice, bob, 100);

            // transfer max amount to another holder
            mptAlice.pay(bob, carol, 100);
        }

        // Simple payment
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});

            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            // issuer to holder
            mptAlice.pay(alice, bob, 100);

            // holder to issuer
            mptAlice.pay(bob, alice, 100);

            // holder to holder
            mptAlice.pay(alice, bob, 100);
            mptAlice.pay(bob, carol, 50);
        }
    }

    void
    testDepositPreauth(FeatureBitset features)
    {
        using namespace test::jtx;
        Account const alice("alice");  // issuer
        Account const bob("bob");      // holder
        Account const diana("diana");
        Account const dpIssuer("dpIssuer");  // holder

        char const credType[] = "abcde";

        if (features[featureCredentials])
        {
            testcase("DepositPreauth");

            Env env(*this, features);

            env.fund(XRP(50000), diana, dpIssuer);
            env.close();

            MPTTester mptAlice(env, alice, {.holders = {bob}});
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTRequireAuth | tfMPTCanTransfer});

            env(pay(diana, bob, XRP(500)));
            env.close();

            // bob creates an empty MPToken
            mptAlice.authorize({.account = bob});
            // alice authorizes bob to hold funds
            mptAlice.authorize({.account = alice, .holder = bob});

            // Bob require preauthorization
            env(fset(bob, asfDepositAuth));
            env.close();

            // alice try to send 100 MPT to bob, not authorized
            mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
            env.close();

            // Bob authorize alice
            env(deposit::auth(bob, alice));
            env.close();

            // alice sends 100 MPT to bob
            mptAlice.pay(alice, bob, 100);
            env.close();

            // Create credentials
            env(credentials::create(alice, dpIssuer, credType));
            env.close();
            env(credentials::accept(alice, dpIssuer, credType));
            env.close();
            auto const jv =
                credentials::ledgerEntry(env, alice, dpIssuer, credType);
            std::string const credIdx = jv[jss::result][jss::index].asString();

            // alice sends 100 MPT to bob with credentials which aren't required
            mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
            env.close();

            // Bob revoke authorization
            env(deposit::unauth(bob, alice));
            env.close();

            // alice try to send 100 MPT to bob, not authorized
            mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
            env.close();

            // alice sends 100 MPT to bob with credentials, not authorized
            mptAlice.pay(alice, bob, 100, tecNO_PERMISSION, {{credIdx}});
            env.close();

            // Bob authorize credentials
            env(deposit::authCredentials(bob, {{dpIssuer, credType}}));
            env.close();

            // alice try to send 100 MPT to bob, not authorized
            mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
            env.close();

            // alice sends 100 MPT to bob with credentials
            mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
            env.close();
        }

        testcase("DepositPreauth disabled featureCredentials");
        {
            Env env(*this, testable_amendments() - featureCredentials);

            std::string const credIdx =
                "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6"
                "E2";

            env.fund(XRP(50000), diana, dpIssuer);
            env.close();

            MPTTester mptAlice(env, alice, {.holders = {bob}});
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTRequireAuth | tfMPTCanTransfer});

            env(pay(diana, bob, XRP(500)));
            env.close();

            // bob creates an empty MPToken
            mptAlice.authorize({.account = bob});
            // alice authorizes bob to hold funds
            mptAlice.authorize({.account = alice, .holder = bob});

            // Bob require preauthorization
            env(fset(bob, asfDepositAuth));
            env.close();

            // alice try to send 100 MPT to bob, not authorized
            mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
            env.close();

            // alice try to send 100 MPT to bob with credentials, amendment
            // disabled
            mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
            env.close();

            // Bob authorize alice
            env(deposit::auth(bob, alice));
            env.close();

            // alice sends 100 MPT to bob
            mptAlice.pay(alice, bob, 100);
            env.close();

            // alice sends 100 MPT to bob with credentials, amendment disabled
            mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
            env.close();

            // Bob revoke authorization
            env(deposit::unauth(bob, alice));
            env.close();

            // alice try to send 100 MPT to bob
            mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
            env.close();

            // alice sends 100 MPT to bob with credentials, amendment disabled
            mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
            env.close();
        }
    }

    void
    testMPTInvalidInTx(FeatureBitset features)
    {
        testcase("MPT Issue Invalid in Transaction");
        using namespace test::jtx;

        // Validate that every transaction with an amount/issue field,
        // which doesn't support MPT, fails.

        // keyed by transaction + amount/issue field
        std::set<std::string> txWithAmounts;
        for (auto const& format : TxFormats::getInstance())
        {
            for (auto const& e : format.getSOTemplate())
            {
                // Transaction has amount/issue fields.
                // Exclude pseudo-transaction SetFee. Don't consider
                // the Fee field since it's included in every transaction.
                if (e.supportMPT() == soeMPTNotSupported &&
                    e.sField().getName() != jss::Fee &&
                    format.getName() != jss::SetFee)
                {
                    txWithAmounts.insert(
                        format.getName() + e.sField().fieldName);
                    break;
                }
            }
        }

        Account const alice("alice");
        auto const USD = alice["USD"];
        Account const carol("carol");
        MPTIssue issue(makeMptID(1, alice));
        STAmount mpt{issue, UINT64_C(100)};
        auto const jvb = bridge(alice, USD, alice, USD);
        for (auto const& feature : {features, features - featureMPTokensV1})
        {
            Env env{*this, feature};
            env.fund(XRP(1'000), alice);
            env.fund(XRP(1'000), carol);
            auto test = [&](Json::Value const& jv,
                            std::string const& mptField) {
                txWithAmounts.erase(
                    jv[jss::TransactionType].asString() + mptField);

                // tx is signed
                auto jtx = env.jt(jv);
                Serializer s;
                jtx.stx->add(s);
                auto jrr = env.rpc("submit", strHex(s.slice()));
                BEAST_EXPECT(
                    jrr[jss::result][jss::error] == "invalidTransaction");

                // tx is unsigned
                Json::Value jv1;
                jv1[jss::secret] = alice.name();
                jv1[jss::tx_json] = jv;
                jrr = env.rpc("json", "submit", to_string(jv1));
                BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");

                jrr = env.rpc("json", "sign", to_string(jv1));
                BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
            };
            auto toSFieldRef = [](SField const& field) {
                return std::ref(field);
            };
            auto setMPTFields = [&](SField const& field,
                                    Json::Value& jv,
                                    bool withAmount = true) {
                jv[jss::Asset] = to_json(xrpIssue());
                jv[jss::Asset2] = to_json(USD.issue());
                if (withAmount)
                    jv[field.fieldName] =
                        USD(10).value().getJson(JsonOptions::none);
                if (field == sfAsset)
                    jv[jss::Asset] = to_json(mpt.get<MPTIssue>());
                else if (field == sfAsset2)
                    jv[jss::Asset2] = to_json(mpt.get<MPTIssue>());
                else
                    jv[field.fieldName] = mpt.getJson(JsonOptions::none);
            };
            // All transactions with sfAmount, which don't support MPT.
            // Transactions with amount fields, which can't be MPT.
            // Transactions with issue fields, which can't be MPT.

            // AMMCreate
            auto ammCreate = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::AMMCreate;
                jv[jss::Account] = alice.human();
                jv[jss::Amount] = (field.fieldName == sfAmount.fieldName)
                    ? mpt.getJson(JsonOptions::none)
                    : "100000000";
                jv[jss::Amount2] = (field.fieldName == sfAmount2.fieldName)
                    ? mpt.getJson(JsonOptions::none)
                    : "100000000";
                jv[jss::TradingFee] = 0;
                test(jv, field.fieldName);
            };
            ammCreate(sfAmount);
            ammCreate(sfAmount2);
            // AMMDeposit
            auto ammDeposit = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::AMMDeposit;
                jv[jss::Account] = alice.human();
                jv[jss::Flags] = tfSingleAsset;
                setMPTFields(field, jv);
                test(jv, field.fieldName);
            };
            for (SField const& field :
                 {toSFieldRef(sfAmount),
                  toSFieldRef(sfAmount2),
                  toSFieldRef(sfEPrice),
                  toSFieldRef(sfLPTokenOut),
                  toSFieldRef(sfAsset),
                  toSFieldRef(sfAsset2)})
                ammDeposit(field);
            // AMMWithdraw
            auto ammWithdraw = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::AMMWithdraw;
                jv[jss::Account] = alice.human();
                jv[jss::Flags] = tfSingleAsset;
                setMPTFields(field, jv);
                test(jv, field.fieldName);
            };
            ammWithdraw(sfAmount);
            for (SField const& field :
                 {toSFieldRef(sfAmount2),
                  toSFieldRef(sfEPrice),
                  toSFieldRef(sfLPTokenIn),
                  toSFieldRef(sfAsset),
                  toSFieldRef(sfAsset2)})
                ammWithdraw(field);
            // AMMBid
            auto ammBid = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::AMMBid;
                jv[jss::Account] = alice.human();
                setMPTFields(field, jv);
                test(jv, field.fieldName);
            };
            for (SField const& field :
                 {toSFieldRef(sfBidMin),
                  toSFieldRef(sfBidMax),
                  toSFieldRef(sfAsset),
                  toSFieldRef(sfAsset2)})
                ammBid(field);
            // AMMClawback
            auto ammClawback = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::AMMClawback;
                jv[jss::Account] = alice.human();
                jv[jss::Holder] = carol.human();
                setMPTFields(field, jv);
                test(jv, field.fieldName);
            };
            for (SField const& field :
                 {toSFieldRef(sfAmount),
                  toSFieldRef(sfAsset),
                  toSFieldRef(sfAsset2)})
                ammClawback(field);
            // AMMDelete
            auto ammDelete = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::AMMDelete;
                jv[jss::Account] = alice.human();
                setMPTFields(field, jv, false);
                test(jv, field.fieldName);
            };
            ammDelete(sfAsset);
            ammDelete(sfAsset2);
            // AMMVote
            auto ammVote = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::AMMVote;
                jv[jss::Account] = alice.human();
                jv[jss::TradingFee] = 100;
                setMPTFields(field, jv, false);
                test(jv, field.fieldName);
            };
            ammVote(sfAsset);
            ammVote(sfAsset2);
            // CheckCash
            auto checkCash = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::CheckCash;
                jv[jss::Account] = alice.human();
                jv[sfCheckID.fieldName] = to_string(uint256{1});
                jv[field.fieldName] = mpt.getJson(JsonOptions::none);
                test(jv, field.fieldName);
            };
            checkCash(sfAmount);
            checkCash(sfDeliverMin);
            // CheckCreate
            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::CheckCreate;
                jv[jss::Account] = alice.human();
                jv[jss::Destination] = carol.human();
                jv[jss::SendMax] = mpt.getJson(JsonOptions::none);
                test(jv, jss::SendMax.c_str());
            }
            // OfferCreate
            {
                Json::Value jv = offer(alice, USD(100), mpt);
                test(jv, jss::TakerPays.c_str());
                jv = offer(alice, mpt, USD(100));
                test(jv, jss::TakerGets.c_str());
            }
            // PaymentChannelCreate
            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::PaymentChannelCreate;
                jv[jss::Account] = alice.human();
                jv[jss::Destination] = carol.human();
                jv[jss::SettleDelay] = 1;
                jv[sfPublicKey.fieldName] = strHex(alice.pk().slice());
                jv[jss::Amount] = mpt.getJson(JsonOptions::none);
                test(jv, jss::Amount.c_str());
            }
            // PaymentChannelFund
            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::PaymentChannelFund;
                jv[jss::Account] = alice.human();
                jv[sfChannel.fieldName] = to_string(uint256{1});
                jv[jss::Amount] = mpt.getJson(JsonOptions::none);
                test(jv, jss::Amount.c_str());
            }
            // PaymentChannelClaim
            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::PaymentChannelClaim;
                jv[jss::Account] = alice.human();
                jv[sfChannel.fieldName] = to_string(uint256{1});
                jv[jss::Amount] = mpt.getJson(JsonOptions::none);
                test(jv, jss::Amount.c_str());
            }
            // NFTokenCreateOffer
            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::NFTokenCreateOffer;
                jv[jss::Account] = alice.human();
                jv[sfNFTokenID.fieldName] = to_string(uint256{1});
                jv[jss::Amount] = mpt.getJson(JsonOptions::none);
                test(jv, jss::Amount.c_str());
            }
            // NFTokenAcceptOffer
            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::NFTokenAcceptOffer;
                jv[jss::Account] = alice.human();
                jv[sfNFTokenBrokerFee.fieldName] =
                    mpt.getJson(JsonOptions::none);
                test(jv, sfNFTokenBrokerFee.fieldName);
            }
            // NFTokenMint
            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::NFTokenMint;
                jv[jss::Account] = alice.human();
                jv[sfNFTokenTaxon.fieldName] = 1;
                jv[jss::Amount] = mpt.getJson(JsonOptions::none);
                test(jv, jss::Amount.c_str());
            }
            // TrustSet
            auto trustSet = [&](SField const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = jss::TrustSet;
                jv[jss::Account] = alice.human();
                jv[jss::Flags] = 0;
                jv[field.fieldName] = mpt.getJson(JsonOptions::none);
                test(jv, field.fieldName);
            };
            trustSet(sfLimitAmount);
            trustSet(sfFee);
            // XChainCommit
            {
                Json::Value const jv = xchain_commit(alice, jvb, 1, mpt);
                test(jv, jss::Amount.c_str());
            }
            // XChainClaim
            {
                Json::Value const jv = xchain_claim(alice, jvb, 1, mpt, alice);
                test(jv, jss::Amount.c_str());
            }
            // XChainCreateClaimID
            {
                Json::Value const jv =
                    xchain_create_claim_id(alice, jvb, mpt, alice);
                test(jv, sfSignatureReward.fieldName);
            }
            // XChainAddClaimAttestation
            {
                Json::Value const jv = claim_attestation(
                    alice,
                    jvb,
                    alice,
                    mpt,
                    alice,
                    true,
                    1,
                    alice,
                    signer(alice));
                test(jv, jss::Amount.c_str());
            }
            // XChainAddAccountCreateAttestation
            {
                Json::Value jv = create_account_attestation(
                    alice,
                    jvb,
                    alice,
                    mpt,
                    XRP(10),
                    alice,
                    false,
                    1,
                    alice,
                    signer(alice));
                for (auto const& field :
                     {sfAmount.fieldName, sfSignatureReward.fieldName})
                {
                    jv[field] = mpt.getJson(JsonOptions::none);
                    test(jv, field);
                }
            }
            // XChainAccountCreateCommit
            {
                Json::Value jv = sidechain_xchain_account_create(
                    alice, jvb, alice, mpt, XRP(10));
                for (auto const& field :
                     {sfAmount.fieldName, sfSignatureReward.fieldName})
                {
                    jv[field] = mpt.getJson(JsonOptions::none);
                    test(jv, field);
                }
            }
            // XChain[Create|Modify]Bridge
            auto bridgeTx = [&](Json::StaticString const& tt,
                                STAmount const& rewardAmount,
                                STAmount const& minAccountAmount,
                                std::string const& field) {
                Json::Value jv;
                jv[jss::TransactionType] = tt;
                jv[jss::Account] = alice.human();
                jv[sfXChainBridge.fieldName] = jvb;
                jv[sfSignatureReward.fieldName] =
                    rewardAmount.getJson(JsonOptions::none);
                jv[sfMinAccountCreateAmount.fieldName] =
                    minAccountAmount.getJson(JsonOptions::none);
                test(jv, field);
            };
            auto reward = STAmount{sfSignatureReward, mpt};
            auto minAmount = STAmount{sfMinAccountCreateAmount, USD(10)};
            for (SField const& field :
                 {std::ref(sfSignatureReward),
                  std::ref(sfMinAccountCreateAmount)})
            {
                bridgeTx(
                    jss::XChainCreateBridge,
                    reward,
                    minAmount,
                    field.fieldName);
                bridgeTx(
                    jss::XChainModifyBridge,
                    reward,
                    minAmount,
                    field.fieldName);
                reward = STAmount{sfSignatureReward, USD(10)};
                minAmount = STAmount{sfMinAccountCreateAmount, mpt};
            }
        }
        BEAST_EXPECT(txWithAmounts.empty());
    }

    void
    testTxJsonMetaFields(FeatureBitset features)
    {
        // checks synthetically injected mptissuanceid from  `tx` response
        testcase("Test synthetic fields from tx response");

        using namespace test::jtx;

        Account const alice{"alice"};

        auto cfg = envconfig();
        cfg->FEES.reference_fee = 10;
        Env env{*this, std::move(cfg), features};
        MPTTester mptAlice(env, alice);

        mptAlice.create();

        std::string const txHash{
            env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
        BEAST_EXPECTS(
            txHash ==
                "E11F0E0CA14219922B7881F060B9CEE67CFBC87E4049A441ED2AE348FF8FAC"
                "0E",
            txHash);
        Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
        auto const id = meta[jss::mpt_issuance_id].asString();
        // Expect mpt_issuance_id field
        BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id));
        BEAST_EXPECT(id == to_string(mptAlice.issuanceID()));
        BEAST_EXPECTS(
            id == "00000004AE123A8556F3CF91154711376AFB0F894F832B3D", id);
    }

    void
    testClawbackValidation(FeatureBitset features)
    {
        testcase("MPT clawback validations");
        using namespace test::jtx;

        // Make sure clawback cannot work when featureMPTokensV1 is disabled
        {
            Env env(*this, features - featureMPTokensV1);
            Account const alice{"alice"};
            Account const bob{"bob"};

            env.fund(XRP(1000), alice, bob);
            env.close();

            auto const USD = alice["USD"];
            auto const mpt = ripple::test::jtx::MPT(
                alice.name(), makeMptID(env.seq(alice), alice));

            env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED));
            env.close();

            env(claw(alice, mpt(5)), ter(temDISABLED));
            env.close();

            env(claw(alice, mpt(5), bob), ter(temDISABLED));
            env.close();
        }

        // Test preflight
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};

            env.fund(XRP(1000), alice, bob);
            env.close();

            auto const USD = alice["USD"];
            auto const mpt = ripple::test::jtx::MPT(
                alice.name(), makeMptID(env.seq(alice), alice));

            // clawing back IOU from a MPT holder fails
            env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED));
            env.close();

            // clawing back MPT without specifying a holder fails
            env(claw(alice, mpt(5)), ter(temMALFORMED));
            env.close();

            // clawing back zero amount fails
            env(claw(alice, mpt(0), bob), ter(temBAD_AMOUNT));
            env.close();

            // alice can't claw back from herself
            env(claw(alice, mpt(5), alice), ter(temMALFORMED));
            env.close();

            // can't clawback negative amount
            env(claw(alice, mpt(-1), bob), ter(temBAD_AMOUNT));
            env.close();
        }

        // Preclaim - clawback fails when MPTCanClawback is disabled on issuance
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // enable asfAllowTrustLineClawback for alice
            env(fset(alice, asfAllowTrustLineClawback));
            env.close();
            env.require(flags(alice, asfAllowTrustLineClawback));

            // Create issuance without enabling clawback
            mptAlice.create({.ownerCount = 1, .holderCount = 0});

            mptAlice.authorize({.account = bob});

            mptAlice.pay(alice, bob, 100);

            // alice cannot clawback before she didn't enable MPTCanClawback
            // asfAllowTrustLineClawback has no effect
            mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
        }

        // Preclaim - test various scenarios
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};
            Account const carol{"carol"};
            env.fund(XRP(1000), carol);
            env.close();
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            auto const fakeMpt = ripple::test::jtx::MPT(
                alice.name(), makeMptID(env.seq(alice), alice));

            // issuer tries to clawback MPT where issuance doesn't exist
            env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND));
            env.close();

            // alice creates issuance
            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});

            // alice tries to clawback from someone who doesn't have MPToken
            mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND);

            // bob creates a MPToken
            mptAlice.authorize({.account = bob});

            // clawback fails because bob currently has a balance of zero
            mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);

            // alice pays bob 100 tokens
            mptAlice.pay(alice, bob, 100);

            // carol fails tries to clawback from bob because he is not the
            // issuer
            mptAlice.claw(carol, bob, 1, tecNO_PERMISSION);
        }

        // clawback more than max amount
        // fails in the json parser before
        // transactor is called
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};

            env.fund(XRP(1000), alice, bob);
            env.close();

            auto const mpt = ripple::test::jtx::MPT(
                alice.name(), makeMptID(env.seq(alice), alice));

            Json::Value jv = claw(alice, mpt(1), bob);
            jv[jss::Amount][jss::value] = to_string(maxMPTokenAmount + 1);
            Json::Value jv1;
            jv1[jss::secret] = alice.name();
            jv1[jss::tx_json] = jv;
            auto const jrr = env.rpc("json", "submit", to_string(jv1));
            BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
        }
    }

    void
    testClawback(FeatureBitset features)
    {
        testcase("MPT Clawback");
        using namespace test::jtx;

        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // alice creates issuance
            mptAlice.create(
                {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});

            // bob creates a MPToken
            mptAlice.authorize({.account = bob});

            // alice pays bob 100 tokens
            mptAlice.pay(alice, bob, 100);

            mptAlice.claw(alice, bob, 1);

            mptAlice.claw(alice, bob, 1000);

            // clawback fails because bob currently has a balance of zero
            mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);
        }

        // Test that globally locked funds can be clawed
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // alice creates issuance
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanLock | tfMPTCanClawback});

            // bob creates a MPToken
            mptAlice.authorize({.account = bob});

            // alice pays bob 100 tokens
            mptAlice.pay(alice, bob, 100);

            mptAlice.set({.account = alice, .flags = tfMPTLock});

            mptAlice.claw(alice, bob, 100);
        }

        // Test that individually locked funds can be clawed
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // alice creates issuance
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanLock | tfMPTCanClawback});

            // bob creates a MPToken
            mptAlice.authorize({.account = bob});

            // alice pays bob 100 tokens
            mptAlice.pay(alice, bob, 100);

            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});

            mptAlice.claw(alice, bob, 100);
        }

        // Test that unauthorized funds can be clawed back
        {
            Env env(*this, features);
            Account const alice{"alice"};
            Account const bob{"bob"};

            MPTTester mptAlice(env, alice, {.holders = {bob}});

            // alice creates issuance
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanClawback | tfMPTRequireAuth});

            // bob creates a MPToken
            mptAlice.authorize({.account = bob});

            // alice authorizes bob
            mptAlice.authorize({.account = alice, .holder = bob});

            // alice pays bob 100 tokens
            mptAlice.pay(alice, bob, 100);

            // alice unauthorizes bob
            mptAlice.authorize(
                {.account = alice, .holder = bob, .flags = tfMPTUnauthorize});

            mptAlice.claw(alice, bob, 100);
        }
    }

    void
    testTokensEquality()
    {
        using namespace test::jtx;
        testcase("Tokens Equality");
        Currency const cur1{to_currency("CU1")};
        Currency const cur2{to_currency("CU2")};
        Account const gw1{"gw1"};
        Account const gw2{"gw2"};
        MPTID const mpt1 = makeMptID(1, gw1);
        MPTID const mpt1a = makeMptID(1, gw1);
        MPTID const mpt2 = makeMptID(1, gw2);
        MPTID const mpt3 = makeMptID(2, gw2);
        Asset const assetCur1Gw1{Issue{cur1, gw1}};
        Asset const assetCur1Gw1a{Issue{cur1, gw1}};
        Asset const assetCur2Gw1{Issue{cur2, gw1}};
        Asset const assetCur2Gw2{Issue{cur2, gw2}};
        Asset const assetMpt1Gw1{mpt1};
        Asset const assetMpt1Gw1a{mpt1a};
        Asset const assetMpt1Gw2{mpt2};
        Asset const assetMpt2Gw2{mpt3};

        // Assets holding Issue
        // Currencies are equal regardless of the issuer
        BEAST_EXPECT(equalTokens(assetCur1Gw1, assetCur1Gw1a));
        BEAST_EXPECT(equalTokens(assetCur2Gw1, assetCur2Gw2));
        // Currencies are different regardless of whether the issuers
        // are the same or not
        BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw1));
        BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw2));

        // Assets holding MPTIssue
        // MPTIDs are the same if the sequence and the issuer are the same
        BEAST_EXPECT(equalTokens(assetMpt1Gw1, assetMpt1Gw1a));
        // MPTIDs are different if sequence and the issuer don't match
        BEAST_EXPECT(!equalTokens(assetMpt1Gw1, assetMpt1Gw2));
        BEAST_EXPECT(!equalTokens(assetMpt1Gw2, assetMpt2Gw2));

        // Assets holding Issue and MPTIssue
        BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetMpt1Gw1));
        BEAST_EXPECT(!equalTokens(assetMpt2Gw2, assetCur2Gw2));
    }

    void
    testHelperFunctions()
    {
        using namespace test::jtx;
        Account const gw{"gw"};
        Asset const asset1{makeMptID(1, gw)};
        Asset const asset2{makeMptID(2, gw)};
        Asset const asset3{makeMptID(3, gw)};
        STAmount const amt1{asset1, 100};
        STAmount const amt2{asset2, 100};
        STAmount const amt3{asset3, 10'000};

        {
            testcase("Test STAmount MPT arithmetics");
            using namespace std::string_literals;
            STAmount res = multiply(amt1, amt2, asset3);
            BEAST_EXPECT(res == amt3);

            res = mulRound(amt1, amt2, asset3, true);
            BEAST_EXPECT(res == amt3);

            res = mulRoundStrict(amt1, amt2, asset3, true);
            BEAST_EXPECT(res == amt3);

            // overflow, any value > 3037000499ull
            STAmount mptOverflow{asset2, UINT64_C(3037000500)};
            try
            {
                res = multiply(mptOverflow, mptOverflow, asset3);
                fail("should throw runtime exception 1");
            }
            catch (std::runtime_error const& e)
            {
                BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
            }
            // overflow, (v1 >> 32) * v2 > 2147483648ull
            mptOverflow = STAmount{asset2, UINT64_C(2147483648)};
            uint64_t const mantissa = (2ull << 32) + 2;
            try
            {
                res = multiply(STAmount{asset1, mantissa}, mptOverflow, asset3);
                fail("should throw runtime exception 2");
            }
            catch (std::runtime_error const& e)
            {
                BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
            }
        }

        {
            testcase("Test MPTAmount arithmetics");
            MPTAmount mptAmt1{100};
            MPTAmount const mptAmt2{100};
            BEAST_EXPECT((mptAmt1 += mptAmt2) == MPTAmount{200});
            BEAST_EXPECT(mptAmt1 == 200);
            BEAST_EXPECT((mptAmt1 -= mptAmt2) == mptAmt1);
            BEAST_EXPECT(mptAmt1 == mptAmt2);
            BEAST_EXPECT(mptAmt1 == 100);
            BEAST_EXPECT(MPTAmount::minPositiveAmount() == MPTAmount{1});
        }

        {
            testcase("Test MPTIssue from/to Json");
            MPTIssue const issue1{asset1.get<MPTIssue>()};
            Json::Value const jv = to_json(issue1);
            BEAST_EXPECT(
                jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
            BEAST_EXPECT(issue1 == mptIssueFromJson(jv));
        }

        {
            testcase("Test Asset from/to Json");
            Json::Value const jv = to_json(asset1);
            BEAST_EXPECT(
                jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
            BEAST_EXPECT(
                to_string(jv) ==
                "{\"mpt_issuance_id\":"
                "\"00000001A407AF5856CCF3C42619DAA925813FC955C72983\"}");
            BEAST_EXPECT(asset1 == assetFromJson(jv));
        }
    }

    void
    testInvalidCreateDynamic(FeatureBitset features)
    {
        testcase("invalid MPTokenIssuanceCreate for DynamicMPT");

        using namespace test::jtx;
        Account const alice("alice");

        // Can not provide MutableFlags when DynamicMPT amendment is not enabled
        {
            Env env{*this, features - featureDynamicMPT};
            MPTTester mptAlice(env, alice);
            mptAlice.create(
                {.ownerCount = 0, .mutableFlags = 2, .err = temDISABLED});
            mptAlice.create(
                {.ownerCount = 0, .mutableFlags = 0, .err = temDISABLED});
        }

        // MutableFlags contains invalid values
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice);

            // Value 1 is reserved for MPT lock.
            mptAlice.create(
                {.ownerCount = 0, .mutableFlags = 1, .err = temINVALID_FLAG});
            mptAlice.create(
                {.ownerCount = 0, .mutableFlags = 17, .err = temINVALID_FLAG});
            mptAlice.create(
                {.ownerCount = 0,
                 .mutableFlags = 65535,
                 .err = temINVALID_FLAG});

            // MutableFlags can not be 0
            mptAlice.create(
                {.ownerCount = 0, .mutableFlags = 0, .err = temINVALID_FLAG});
        }
    }

    void
    testInvalidSetDynamic(FeatureBitset features)
    {
        testcase("invalid MPTokenIssuanceSet for DynamicMPT");

        using namespace test::jtx;
        Account const alice("alice");
        Account const bob("bob");

        // Can not provide MutableFlags, MPTokenMetadata or TransferFee when
        // DynamicMPT amendment is not enabled
        {
            Env env{*this, features - featureDynamicMPT};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            auto const mptID = makeMptID(env.seq(alice), alice);

            // MutableFlags is not allowed when DynamicMPT is not enabled
            mptAlice.set(
                {.account = alice,
                 .id = mptID,
                 .mutableFlags = 2,
                 .err = temDISABLED});
            mptAlice.set(
                {.account = alice,
                 .id = mptID,
                 .mutableFlags = 0,
                 .err = temDISABLED});

            // MPTokenMetadata is not allowed when DynamicMPT is not enabled
            mptAlice.set(
                {.account = alice,
                 .id = mptID,
                 .metadata = "test",
                 .err = temDISABLED});
            mptAlice.set(
                {.account = alice,
                 .id = mptID,
                 .metadata = "",
                 .err = temDISABLED});

            // TransferFee is not allowed when DynamicMPT is not enabled
            mptAlice.set(
                {.account = alice,
                 .id = mptID,
                 .transferFee = 100,
                 .err = temDISABLED});
            mptAlice.set(
                {.account = alice,
                 .id = mptID,
                 .transferFee = 0,
                 .err = temDISABLED});
        }

        // Can not provide holder when MutableFlags, MPTokenMetadata or
        // TransferFee is present
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            auto const mptID = makeMptID(env.seq(alice), alice);

            // Holder is not allowed when MutableFlags is present
            mptAlice.set(
                {.account = alice,
                 .holder = bob,
                 .id = mptID,
                 .mutableFlags = 2,
                 .err = temMALFORMED});

            // Holder is not allowed when MPTokenMetadata is present
            mptAlice.set(
                {.account = alice,
                 .holder = bob,
                 .id = mptID,
                 .metadata = "test",
                 .err = temMALFORMED});

            // Holder is not allowed when TransferFee is present
            mptAlice.set(
                {.account = alice,
                 .holder = bob,
                 .id = mptID,
                 .transferFee = 100,
                 .err = temMALFORMED});
        }

        // Can not set Flags when MutableFlags, MPTokenMetadata or
        // TransferFee is present
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            mptAlice.create(
                {.ownerCount = 1,
                 .mutableFlags = tmfMPTCanMutateMetadata |
                     tmfMPTCanMutateCanLock | tmfMPTCanMutateTransferFee});

            // Setting flags is not allowed when MutableFlags is present
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTCanLock,
                 .mutableFlags = 2,
                 .err = temMALFORMED});

            // Setting flags is not allowed when MPTokenMetadata is present
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTCanLock,
                 .metadata = "test",
                 .err = temMALFORMED});

            // setting flags is not allowed when TransferFee is present
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTCanLock,
                 .transferFee = 100,
                 .err = temMALFORMED});
        }

        // Flags being 0 or tfFullyCanonicalSig is fine
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.transferFee = 10,
                 .ownerCount = 1,
                 .flags = tfMPTCanTransfer,
                 .mutableFlags =
                     tmfMPTCanMutateTransferFee | tmfMPTCanMutateMetadata});

            mptAlice.set(
                {.account = alice,
                 .flags = 0,
                 .transferFee = 100,
                 .metadata = "test"});
            mptAlice.set(
                {.account = alice,
                 .flags = tfFullyCanonicalSig,
                 .transferFee = 200,
                 .metadata = "test2"});
        }

        // Invalid MutableFlags
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            auto const mptID = makeMptID(env.seq(alice), alice);

            for (auto const flags : {10000, 0, 5000})
            {
                mptAlice.set(
                    {.account = alice,
                     .id = mptID,
                     .mutableFlags = flags,
                     .err = temINVALID_FLAG});
            }
        }

        // Can not set and clear the same mutable flag
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            auto const mptID = makeMptID(env.seq(alice), alice);

            auto const flagCombinations = {
                tmfMPTSetCanLock | tmfMPTClearCanLock,
                tmfMPTSetRequireAuth | tmfMPTClearRequireAuth,
                tmfMPTSetCanEscrow | tmfMPTClearCanEscrow,
                tmfMPTSetCanTrade | tmfMPTClearCanTrade,
                tmfMPTSetCanTransfer | tmfMPTClearCanTransfer,
                tmfMPTSetCanClawback | tmfMPTClearCanClawback,
                tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade,
                tmfMPTSetCanTransfer | tmfMPTClearCanTransfer |
                    tmfMPTSetCanEscrow | tmfMPTClearCanClawback};

            for (auto const& mutableFlags : flagCombinations)
            {
                mptAlice.set(
                    {.account = alice,
                     .id = mptID,
                     .mutableFlags = mutableFlags,
                     .err = temINVALID_FLAG});
            }
        }

        // Can not mutate flag which is not mutable
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1});

            auto const mutableFlags = {
                tmfMPTSetCanLock,
                tmfMPTClearCanLock,
                tmfMPTSetRequireAuth,
                tmfMPTClearRequireAuth,
                tmfMPTSetCanEscrow,
                tmfMPTClearCanEscrow,
                tmfMPTSetCanTrade,
                tmfMPTClearCanTrade,
                tmfMPTSetCanTransfer,
                tmfMPTClearCanTransfer,
                tmfMPTSetCanClawback,
                tmfMPTClearCanClawback};

            for (auto const& mutableFlag : mutableFlags)
            {
                mptAlice.set(
                    {.account = alice,
                     .mutableFlags = mutableFlag,
                     .err = tecNO_PERMISSION});
            }
        }

        // Metadata exceeding max length
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});

            std::string metadata(maxMPTokenMetadataLength + 1, 'a');
            mptAlice.set(
                {.account = alice, .metadata = metadata, .err = temMALFORMED});
        }

        // Can not mutate metadata when it is not mutable
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create({.ownerCount = 1});
            mptAlice.set(
                {.account = alice,
                 .metadata = "test",
                 .err = tecNO_PERMISSION});
        }

        // Transfer fee exceeding the max value
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            auto const mptID = makeMptID(env.seq(alice), alice);

            mptAlice.create(
                {.ownerCount = 1, .mutableFlags = tmfMPTCanMutateTransferFee});

            mptAlice.set(
                {.account = alice,
                 .id = mptID,
                 .transferFee = maxTransferFee + 1,
                 .err = temBAD_TRANSFER_FEE});
        }

        // Test setting non-zero transfer fee and clearing MPTCanTransfer at the
        // same time
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.transferFee = 100,
                 .ownerCount = 1,
                 .flags = tfMPTCanTransfer,
                 .mutableFlags =
                     tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});

            // Can not set non-zero transfer fee and clear MPTCanTransfer at the
            // same time
            mptAlice.set(
                {.account = alice,
                 .mutableFlags = tmfMPTClearCanTransfer,
                 .transferFee = 1,
                 .err = temMALFORMED});

            // Can set transfer fee to zero and clear MPTCanTransfer at the same
            // time. tfMPTCanTransfer will be cleared and TransferFee field will
            // be removed.
            mptAlice.set(
                {.account = alice,
                 .mutableFlags = tmfMPTClearCanTransfer,
                 .transferFee = 0});
            BEAST_EXPECT(!mptAlice.isTransferFeePresent());
        }

        // Can not set non-zero transfer fee when MPTCanTransfer is not set
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.ownerCount = 1,
                 .mutableFlags =
                     tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});

            mptAlice.set(
                {.account = alice,
                 .transferFee = 100,
                 .err = tecNO_PERMISSION});

            // Can not set transfer fee even when trying to set MPTCanTransfer
            // at the same time. MPTCanTransfer must be set first, then transfer
            // fee can be set in a separate transaction.
            mptAlice.set(
                {.account = alice,
                 .mutableFlags = tmfMPTSetCanTransfer,
                 .transferFee = 100,
                 .err = tecNO_PERMISSION});
        }

        // Can not mutate transfer fee when it is not mutable
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.transferFee = 10,
                 .ownerCount = 1,
                 .flags = tfMPTCanTransfer});

            mptAlice.set(
                {.account = alice,
                 .transferFee = 100,
                 .err = tecNO_PERMISSION});

            mptAlice.set(
                {.account = alice, .transferFee = 0, .err = tecNO_PERMISSION});
        }

        // Set some flags mutable. Can not mutate the others
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});

            mptAlice.create(
                {.ownerCount = 1,
                 .mutableFlags = tmfMPTCanMutateCanTrade |
                     tmfMPTCanMutateCanTransfer | tmfMPTCanMutateMetadata});

            // Can not mutate transfer fee
            mptAlice.set(
                {.account = alice,
                 .transferFee = 100,
                 .err = tecNO_PERMISSION});

            auto const invalidFlags = {
                tmfMPTSetCanLock,
                tmfMPTClearCanLock,
                tmfMPTSetRequireAuth,
                tmfMPTClearRequireAuth,
                tmfMPTSetCanEscrow,
                tmfMPTClearCanEscrow,
                tmfMPTSetCanClawback,
                tmfMPTClearCanClawback};

            // Can not mutate flags which are not mutable
            for (auto const& mutableFlag : invalidFlags)
            {
                mptAlice.set(
                    {.account = alice,
                     .mutableFlags = mutableFlag,
                     .err = tecNO_PERMISSION});
            }

            // Can mutate MPTCanTrade
            mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanTrade});

            // Can mutate MPTCanTransfer
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTSetCanTransfer});
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanTransfer});

            // Can mutate metadata
            mptAlice.set({.account = alice, .metadata = "test"});
            mptAlice.set({.account = alice, .metadata = ""});
        }
    }

    void
    testMutateMPT(FeatureBitset features)
    {
        testcase("Mutate MPT");
        using namespace test::jtx;

        Account const alice("alice");

        // Mutate metadata
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice);
            mptAlice.create(
                {.metadata = "test",
                 .ownerCount = 1,
                 .mutableFlags = tmfMPTCanMutateMetadata});

            std::vector<std::string> metadatas = {
                "mutate metadata",
                "mutate metadata 2",
                "mutate metadata 3",
                "mutate metadata 3",
                "test",
                "mutate metadata"};

            for (auto const& metadata : metadatas)
            {
                mptAlice.set({.account = alice, .metadata = metadata});
                BEAST_EXPECT(mptAlice.checkMetadata(metadata));
            }

            // Metadata being empty will remove the field
            mptAlice.set({.account = alice, .metadata = ""});
            BEAST_EXPECT(!mptAlice.isMetadataPresent());
        }

        // Mutate transfer fee
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice);
            mptAlice.create(
                {.transferFee = 100,
                 .metadata = "test",
                 .ownerCount = 1,
                 .flags = tfMPTCanTransfer,
                 .mutableFlags = tmfMPTCanMutateTransferFee});

            for (std::uint16_t const fee : std::initializer_list<std::uint16_t>{
                     1, 10, 100, 200, 500, 1000, maxTransferFee})
            {
                mptAlice.set({.account = alice, .transferFee = fee});
                BEAST_EXPECT(mptAlice.checkTransferFee(fee));
            }

            // Setting TransferFee to zero will remove the field
            mptAlice.set({.account = alice, .transferFee = 0});
            BEAST_EXPECT(!mptAlice.isTransferFeePresent());

            // Set transfer fee again
            mptAlice.set({.account = alice, .transferFee = 10});
            BEAST_EXPECT(mptAlice.checkTransferFee(10));
        }

        // Test flag toggling
        {
            auto testFlagToggle = [&](std::uint32_t createFlags,
                                      std::uint32_t setFlags,
                                      std::uint32_t clearFlags) {
                Env env{*this, features};
                MPTTester mptAlice(env, alice);

                // Create the MPT object with the specified initial flags
                mptAlice.create(
                    {.metadata = "test",
                     .ownerCount = 1,
                     .mutableFlags = createFlags});

                // Set and clear the flag multiple times
                mptAlice.set({.account = alice, .mutableFlags = setFlags});
                mptAlice.set({.account = alice, .mutableFlags = clearFlags});
                mptAlice.set({.account = alice, .mutableFlags = clearFlags});
                mptAlice.set({.account = alice, .mutableFlags = setFlags});
                mptAlice.set({.account = alice, .mutableFlags = setFlags});
                mptAlice.set({.account = alice, .mutableFlags = clearFlags});
                mptAlice.set({.account = alice, .mutableFlags = setFlags});
                mptAlice.set({.account = alice, .mutableFlags = clearFlags});
            };

            testFlagToggle(
                tmfMPTCanMutateCanLock, tfMPTCanLock, tmfMPTClearCanLock);
            testFlagToggle(
                tmfMPTCanMutateRequireAuth,
                tmfMPTSetRequireAuth,
                tmfMPTClearRequireAuth);
            testFlagToggle(
                tmfMPTCanMutateCanEscrow,
                tmfMPTSetCanEscrow,
                tmfMPTClearCanEscrow);
            testFlagToggle(
                tmfMPTCanMutateCanTrade,
                tmfMPTSetCanTrade,
                tmfMPTClearCanTrade);
            testFlagToggle(
                tmfMPTCanMutateCanTransfer,
                tmfMPTSetCanTransfer,
                tmfMPTClearCanTransfer);
            testFlagToggle(
                tmfMPTCanMutateCanClawback,
                tmfMPTSetCanClawback,
                tmfMPTClearCanClawback);
        }
    }

    void
    testMutateCanLock(FeatureBitset features)
    {
        testcase("Mutate MPTCanLock");
        using namespace test::jtx;

        Account const alice("alice");
        Account const bob("bob");

        // Individual lock
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanLock | tfMPTCanTransfer,
                 .mutableFlags = tmfMPTCanMutateCanLock |
                     tmfMPTCanMutateCanTrade | tmfMPTCanMutateTransferFee});
            mptAlice.authorize({.account = bob, .holderCount = 1});

            // Lock bob's mptoken
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});

            // Can mutate the mutable flags and fields
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanLock});
            mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanLock});
            mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanTrade});
            mptAlice.set({.account = alice, .transferFee = 200});
        }

        // Global lock
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanLock,
                 .mutableFlags = tmfMPTCanMutateCanLock |
                     tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata});
            mptAlice.authorize({.account = bob, .holderCount = 1});

            // Lock issuance
            mptAlice.set({.account = alice, .flags = tfMPTLock});

            // Can mutate the mutable flags and fields
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanLock});
            mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanLock});
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTSetCanClawback});
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanClawback});
            mptAlice.set({.account = alice, .metadata = "mutate"});
        }

        // Test lock and unlock after mutating MPTCanLock
        {
            Env env{*this, features};
            MPTTester mptAlice(env, alice, {.holders = {bob}});
            mptAlice.create(
                {.ownerCount = 1,
                 .holderCount = 0,
                 .flags = tfMPTCanLock,
                 .mutableFlags = tmfMPTCanMutateCanLock |
                     tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata});
            mptAlice.authorize({.account = bob, .holderCount = 1});

            // Can lock and unlock
            mptAlice.set({.account = alice, .flags = tfMPTLock});
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
            mptAlice.set({.account = alice, .flags = tfMPTUnlock});
            mptAlice.set(
                {.account = alice, .holder = bob, .flags = tfMPTUnlock});

            // Clear lsfMPTCanLock
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanLock});

            // Can not lock or unlock
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTLock,
                 .err = tecNO_PERMISSION});
            mptAlice.set(
                {.account = alice,
                 .flags = tfMPTUnlock,
                 .err = tecNO_PERMISSION});
            mptAlice.set(
                {.account = alice,
                 .holder = bob,
                 .flags = tfMPTLock,
                 .err = tecNO_PERMISSION});
            mptAlice.set(
                {.account = alice,
                 .holder = bob,
                 .flags = tfMPTUnlock,
                 .err = tecNO_PERMISSION});

            // Set MPTCanLock again
            mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});

            // Can lock and unlock again
            mptAlice.set({.account = alice, .flags = tfMPTLock});
            mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
            mptAlice.set({.account = alice, .flags = tfMPTUnlock});
            mptAlice.set(
                {.account = alice, .holder = bob, .flags = tfMPTUnlock});
        }
    }

    void
    testMutateRequireAuth(FeatureBitset features)
    {
        testcase("Mutate MPTRequireAuth");
        using namespace test::jtx;

        Env env{*this, features};
        Account const alice("alice");
        Account const bob("bob");

        MPTTester mptAlice(env, alice, {.holders = {bob}});
        mptAlice.create(
            {.ownerCount = 1,
             .flags = tfMPTRequireAuth,
             .mutableFlags = tmfMPTCanMutateRequireAuth});

        mptAlice.authorize({.account = bob});
        mptAlice.authorize({.account = alice, .holder = bob});

        // Pay to bob
        mptAlice.pay(alice, bob, 1000);

        // Unauthorize bob
        mptAlice.authorize(
            {.account = alice, .holder = bob, .flags = tfMPTUnauthorize});

        // Can not pay to bob
        mptAlice.pay(bob, alice, 100, tecNO_AUTH);

        // Clear RequireAuth
        mptAlice.set(
            {.account = alice, .mutableFlags = tmfMPTClearRequireAuth});

        // Can pay to bob
        mptAlice.pay(alice, bob, 1000);

        // Set RequireAuth again
        mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});

        // Can not pay to bob since he is not authorized
        mptAlice.pay(bob, alice, 100, tecNO_AUTH);

        // Authorize bob again
        mptAlice.authorize({.account = alice, .holder = bob});

        // Can pay to bob again
        mptAlice.pay(alice, bob, 100);
    }

    void
    testMutateCanEscrow(FeatureBitset features)
    {
        testcase("Mutate MPTCanEscrow");
        using namespace test::jtx;
        using namespace std::literals;

        Env env{*this, features};
        auto const baseFee = env.current()->fees().base;
        auto const alice = Account("alice");
        auto const bob = Account("bob");
        auto const carol = Account("carol");

        MPTTester mptAlice(env, alice, {.holders = {carol, bob}});
        mptAlice.create(
            {.ownerCount = 1,
             .holderCount = 0,
             .flags = tfMPTCanTransfer,
             .mutableFlags = tmfMPTCanMutateCanEscrow});
        mptAlice.authorize({.account = carol});
        mptAlice.authorize({.account = bob});

        auto const MPT = mptAlice["MPT"];
        env(pay(alice, carol, MPT(10'000)));
        env(pay(alice, bob, MPT(10'000)));
        env.close();

        // MPTCanEscrow is not enabled
        env(escrow::create(carol, bob, MPT(3)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150),
            ter(tecNO_PERMISSION));

        // MPTCanEscrow is enabled now
        mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanEscrow});
        env(escrow::create(carol, bob, MPT(3)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150));

        // Clear MPTCanEscrow
        mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanEscrow});
        env(escrow::create(carol, bob, MPT(3)),
            escrow::condition(escrow::cb1),
            escrow::finish_time(env.now() + 1s),
            fee(baseFee * 150),
            ter(tecNO_PERMISSION));
    }

    void
    testMutateCanTransfer(FeatureBitset features)
    {
        testcase("Mutate MPTCanTransfer");

        using namespace test::jtx;
        Account const alice("alice");
        Account const bob("bob");
        Account const carol("carol");

        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
            mptAlice.create(
                {.ownerCount = 1,
                 .mutableFlags =
                     tmfMPTCanMutateCanTransfer | tmfMPTCanMutateTransferFee});

            mptAlice.authorize({.account = bob});
            mptAlice.authorize({.account = carol});

            // Pay to bob
            mptAlice.pay(alice, bob, 1000);

            // Bob can not pay carol since MPTCanTransfer is not set
            mptAlice.pay(bob, carol, 50, tecNO_AUTH);

            // Can not set non-zero transfer fee when MPTCanTransfer is not set
            mptAlice.set(
                {.account = alice,
                 .transferFee = 100,
                 .err = tecNO_PERMISSION});

            // Can not set non-zero transfer fee even when trying to set
            // MPTCanTransfer at the same time
            mptAlice.set(
                {.account = alice,
                 .mutableFlags = tmfMPTSetCanTransfer,
                 .transferFee = 100,
                 .err = tecNO_PERMISSION});

            // Alice sets MPTCanTransfer
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTSetCanTransfer});

            // Can set transfer fee now
            BEAST_EXPECT(!mptAlice.isTransferFeePresent());
            mptAlice.set({.account = alice, .transferFee = 100});
            BEAST_EXPECT(mptAlice.isTransferFeePresent());

            // Bob can pay carol
            mptAlice.pay(bob, carol, 50);

            // Alice clears MPTCanTransfer
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanTransfer});

            // TransferFee field is removed when MPTCanTransfer is cleared
            BEAST_EXPECT(!mptAlice.isTransferFeePresent());

            // Bob can not pay
            mptAlice.pay(bob, carol, 50, tecNO_AUTH);
        }

        // Can set transfer fee to zero when MPTCanTransfer is not set, but
        // tmfMPTCanMutateTransferFee is set.
        {
            Env env{*this, features};

            MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
            mptAlice.create(
                {.transferFee = 100,
                 .ownerCount = 1,
                 .flags = tfMPTCanTransfer,
                 .mutableFlags =
                     tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});

            BEAST_EXPECT(mptAlice.checkTransferFee(100));

            // Clear MPTCanTransfer and transfer fee is removed
            mptAlice.set(
                {.account = alice, .mutableFlags = tmfMPTClearCanTransfer});
            BEAST_EXPECT(!mptAlice.isTransferFeePresent());

            // Can still set transfer fee to zero, although it is already zero
            mptAlice.set({.account = alice, .transferFee = 0});

            // TransferFee field is still not present
            BEAST_EXPECT(!mptAlice.isTransferFeePresent());
        }
    }

    void
    testMutateCanClawback(FeatureBitset features)
    {
        testcase("Mutate MPTCanClawback");

        using namespace test::jtx;
        Env env(*this, features);
        Account const alice{"alice"};
        Account const bob{"bob"};

        MPTTester mptAlice(env, alice, {.holders = {bob}});

        mptAlice.create(
            {.ownerCount = 1,
             .holderCount = 0,
             .mutableFlags = tmfMPTCanMutateCanClawback});

        // Bob creates an MPToken
        mptAlice.authorize({.account = bob});

        // Alice pays bob 100 tokens
        mptAlice.pay(alice, bob, 100);

        // MPTCanClawback is not enabled
        mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);

        // Enable MPTCanClawback
        mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback});

        // Can clawback now
        mptAlice.claw(alice, bob, 1);

        // Clear MPTCanClawback
        mptAlice.set(
            {.account = alice, .mutableFlags = tmfMPTClearCanClawback});

        // Can not clawback
        mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
    }

public:
    void
    run() override
    {
        using namespace test::jtx;
        FeatureBitset const all{testable_amendments()};

        // MPTokenIssuanceCreate
        testCreateValidation(all - featureSingleAssetVault);
        testCreateValidation(all - featurePermissionedDomains);
        testCreateValidation(all);
        testCreateEnabled(all - featureSingleAssetVault);
        testCreateEnabled(all);

        // MPTokenIssuanceDestroy
        testDestroyValidation(all - featureSingleAssetVault);
        testDestroyValidation(all);
        testDestroyEnabled(all - featureSingleAssetVault);
        testDestroyEnabled(all);

        // MPTokenAuthorize
        testAuthorizeValidation(all - featureSingleAssetVault);
        testAuthorizeValidation(all);
        testAuthorizeEnabled(all - featureSingleAssetVault);
        testAuthorizeEnabled(all);

        // MPTokenIssuanceSet
        testSetValidation(all - featureSingleAssetVault - featureDynamicMPT);
        testSetValidation(all - featureSingleAssetVault);
        testSetValidation(all - featureDynamicMPT);
        testSetValidation(all - featurePermissionedDomains);
        testSetValidation(all);

        testSetEnabled(all - featureSingleAssetVault);
        testSetEnabled(all);

        // MPT clawback
        testClawbackValidation(all);
        testClawback(all);

        // Test Direct Payment
        testPayment(all);
        testDepositPreauth(all);
        testDepositPreauth(all - featureCredentials);

        // Test MPT Amount is invalid in Tx, which don't support MPT
        testMPTInvalidInTx(all);

        // Test parsed MPTokenIssuanceID in API response metadata
        testTxJsonMetaFields(all);

        // Test tokens equality
        testTokensEquality();

        // Test helpers
        testHelperFunctions();

        // Dynamic MPT
        testInvalidCreateDynamic(all);
        testInvalidSetDynamic(all);
        testMutateMPT(all);
        testMutateCanLock(all);
        testMutateRequireAuth(all);
        testMutateCanEscrow(all);
        testMutateCanTransfer(all);
        testMutateCanClawback(all);
    }
};

BEAST_DEFINE_TESTSUITE_PRIO(MPToken, app, ripple, 2);

}  // namespace test
}  // namespace ripple
